﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Simulation
{
    class Simulation
	{

		private Random rng;

		#region properties
		private int _clock;
		private int _endTime;
        private List<Queue<Customer>> _queues;
        private List<Server> _servers;
        private PriorityQueue<Event> _futureEventList;
		private EventGenerator _eg;
		private int _totalCustomers;
		private int _customersServed;
		private double _meanServeTime;
		private double _stdDevServeTime;
		private int _custumersArived;
		private SimType _simType;
		private QueueingMethod _qm;
		private int _customersKilled;
		private Customer _departingCustomer;
		#endregion

		#region stats
		//Throughput, response time, average wait time, server utilization
		private int _totalWaitTime = 0;
		private int _totalResponseTime = 0;
		private double _throughput = 0;
		private double _serverUtilization = 0;
		private int[] _totalServerFreeTime;
		private int[] _startOfServerFreeTime;
		private bool[] _serverPreviousBusyState;
		#endregion

		public enum SimType{MultiServerSingleQueue, MultiServerMultiQueue}

		public enum QueueingMethod {Random, SQF, LQF, RR}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="meanInterarrivalTime">Interarrival time in minutes</param>
		/// <param name="meanServiceTime">Service time in minutes</param>
		/// <param name="SIGMA">Standard deviation of service time</param>
		/// <param name="totalCustomers">Simulation stops after this many customers have been serviced</param>
		/// <param name="clock">Start time of simulation</param>
		/// <param name="numQueues">Number of queues</param>
		/// <param name="numServers">Number of servers</param>
		/// <param name="endTime">Simulation ends once this time is reached</param>
        public Simulation(SimType simType, QueueingMethod qm, double meanInterarrivalTime, double stdDevArriveTime, double meanServiceTime, double stdDevServeTime, int totalCustomers = -1,
			int clock = 0, int numServers = 1, int endTime = -1)
        {

			if( totalCustomers == -1 && endTime == -1)
				throw(new SystemException("Total customers and end time undefined."));

			rng = new Random();
			_totalServerFreeTime = new int[numServers];
			_startOfServerFreeTime = new int[numServers];
			_serverPreviousBusyState = new bool[numServers];
			//all servers start off free
			for (int i = 0; i < numServers; i++)
			{
				_startOfServerFreeTime[i] = 0;
				_serverPreviousBusyState[i] = false;
			}

            _clock = clock;
			_custumersArived = 0;
			_totalCustomers = totalCustomers;
			_endTime = endTime;
			_meanServeTime = meanServiceTime;
			_stdDevServeTime = stdDevServeTime;
			_simType = simType;
			_qm = qm;

			_queues = new List<Queue<Customer>>();

			//if its multiqueue then create a queue for each server
			if (simType == SimType.MultiServerMultiQueue)
			{
				for (int i = 0; i < numServers; i++)
					_queues.Add(new Queue<Customer>());
			}
				//else just one queue
			else { _queues.Add(new Queue<Customer>()); }

        	_servers = new List<Server>();
			for(int i = 0; i < numServers; i++)
				_servers.Add(new Server());


			//Generate arrivals
			_eg = new EventGenerator();
			_futureEventList = _eg.GenerateArrivals(meanInterarrivalTime, totalCustomers, endTime, stdDevArriveTime);

			//find killed customers
			int time = 0;
			foreach (Event arrival in _futureEventList)
			{
				if (arrival._executionTime == time)
					_customersKilled++;
				time = arrival._executionTime;
			}

        }

		/// <summary>
		/// Little function to determine if the simulation has run for long enough
		/// </summary>
		/// <returns></returns>
		private bool SimulationOver()
		{
			if (_totalCustomers != -1 && _customersServed < _totalCustomers)
				return false;
			if (_endTime != -1 && _clock < _endTime)
				return false;
			return true;
		}

		/// <summary>
		/// Kicks off the simulation and generates the report
		/// </summary>
		public void Run()
		{

			Event currentEvent;
			//while there are still more customers to be had
			while (!SimulationOver())
			{
				//get next event from FEL
				currentEvent = _futureEventList.Dequeue();

				//advance clock to current event time
				_clock = currentEvent._executionTime;

				//execute appropriate event subroutine
				switch (currentEvent._type)
				{
					case EventTypes.Arrival:
						Arrival(currentEvent, _qm);
						_custumersArived++;
						break;
					case EventTypes.Departure:
						Departure(currentEvent);
						_customersServed++;
						break;
				}

				//Check servers and adjust queues as neccessary
				UpdateQueues();
				CollectStats();
			}

			PrintReport();
		}

		private void UpdateQueues()
		{
			#region MultiServerMultiQueue
			if (_simType == SimType.MultiServerMultiQueue)
			{
				for (int i = 0; i < _servers.Count(); i++)
				{
					//if the server is free for that line
					if (!_servers[i]._busy)
					{
						//if there are customers dequeue that line and generate a departure event
						if (_queues[i].Count() > 0)
						{
							Customer c = _queues[i].Dequeue();
							c._clockAtServeTime = _clock;
							_futureEventList.Enqueue(new Event(_clock + c._serviceTime, EventTypes.Departure, c._ID));
							_servers[i]._customer = c;
							_servers[i]._busy = true;
							_servers[i]._numServed++;
						}
					}
				}
			}
			#endregion
			#region MultiServerSingleQueue
			else if (_simType == SimType.MultiServerSingleQueue)
			{
				for (int i = 0; i < _servers.Count(); i++)
				{
					//if any server is free
					if (!_servers[i]._busy)
					{
						//if there are customers dequeue the line and generate a departure event
						if (_queues[0].Count() > 0)
						{
							Customer c = _queues[0].Dequeue();
							c._clockAtServeTime = _clock;
							_futureEventList.Enqueue(new Event(_clock + c._serviceTime, EventTypes.Departure, c._ID));
							_servers[i]._customer = c;
							_servers[i]._busy = true;
							_servers[i]._numServed++;
							//Console.WriteLine("Customer {0} served by Server {1}", c._ID, i);
						}
					}
				}
			}
			#endregion
		}

		/// <summary>
		/// Creates a customer and puts it in the queue. Customer service time is generated here.
		/// </summary>
		/// <param name="e"></param>
		private void Arrival(Event e, QueueingMethod qm)
		{
			int joinedQueue = 0;
			int shortestQueueLength = int.MaxValue;
			int longestQueueLength = 0;

			switch (qm)
			{
				//Join a random queue
				case QueueingMethod.Random:
					joinedQueue = rng.Next(_queues.Count());
					break;

				//join the shorstest queue
				case QueueingMethod.SQF:
					for (int i = 0; i < _queues.Count(); i++)
					{
						if (_queues[i].Count() < shortestQueueLength)
						{
							joinedQueue = i;
							shortestQueueLength = _queues[i].Count;
						}
					}
					break;

				//join the longest queue
				case QueueingMethod.LQF:
					for (int i = 0; i < _queues.Count(); i++)
					{
						if (_queues[i].Count() >= longestQueueLength)
						{
							joinedQueue = i;
							longestQueueLength = _queues[i].Count;
						}
					}
					break;

				//round robin that shit
				default:
					joinedQueue = _custumersArived % _queues.Count();
					break;

			}

			Customer c = new Customer(e._customerID, (int)SimpleRNG.GetNormal(_meanServeTime, _stdDevServeTime));
			c._arrivalTime = _clock;
			//Console.WriteLine("Service time: {0}", c._serviceTime);
			Console.WriteLine("Customer {0} arrived at time {1}. Joined queue {2}.", e._customerID, _clock, joinedQueue);

			_queues[joinedQueue].Enqueue(c);
		}

		/// <summary>
		/// Frees up the server and departs the customer
		/// </summary>
		/// <param name="e"></param>
		private void Departure(Event e)
		{
			//find which server this appies to
			for (int i = 0; i < _servers.Count; i++)
			{
				if (_servers[i]._customer != null && _servers[i]._customer._ID == e._customerID)
				{
					//make that customer the new departing customer (for stats gathering)
					_departingCustomer = _servers[i]._customer;
					//and free up that server
					_servers[i]._busy = false;
					_servers[i]._customer = null;
				}
			}
			Console.WriteLine("Customer {0} departed at time {1}.", e._customerID, _clock);
			
		}

		private void CollectStats()
		{

			//average wait time
			//grab the departing user and add to the average if there is one
			if (_departingCustomer != null)
			{
				_totalWaitTime += (_departingCustomer._clockAtServeTime - _departingCustomer._arrivalTime);
				//grab the response time of the departing customer
				_totalResponseTime += (_clock - _departingCustomer._arrivalTime);
			}

			//remove the departing customer after stats are collected
			_departingCustomer = null;

			//server utilization time
			for (int i = 0; i < _servers.Count(); i++)
			{
				//if the server was free before and is now busy then add the free time to the total free time
				if (_servers[i]._busy == true && _serverPreviousBusyState[i] == false)
					_totalServerFreeTime[i] += (_clock - _startOfServerFreeTime[i]);
				//if the server was busy before and is now free then note the start of free time
				else if (_servers[i]._busy == false && _serverPreviousBusyState[i] == true)
					_startOfServerFreeTime[i] = _clock;

				//mark the current busy states for future reference
				_serverPreviousBusyState[i] = _servers[i]._busy;
			}
		}

		private void PrintReport()
		{
			Console.WriteLine();
			Console.WriteLine("*******************************************************");
			Console.WriteLine("Simulation type: {0}", _simType);
			Console.WriteLine("Queueing Method: {0}", _qm);
			Console.WriteLine("Number of customers: {0}", _customersServed);
			Console.WriteLine("Customers killed: {0}", _customersKilled);
			Console.WriteLine("Time elapsed: {0} minutes", _clock);
			Console.WriteLine("Average wait time: {0:N2} minutes", (double)_totalWaitTime/_customersServed);
			Console.WriteLine("Average response time: {0:N2} minutes", (double)_totalResponseTime / _customersServed);
			Console.WriteLine("Throughput: {0} customers per hour", Math.Floor((_customersServed / ((double)_clock / 60))));
			for (int i = 0; i < _servers.Count(); i++)
			{
				if(_totalServerFreeTime[i] > 0)
					Console.WriteLine("Server {0} Utilization: {1:N2}%. Customers served: {2}.", i, (1 - ((double)_totalServerFreeTime[i] / (double)_clock)) * 100, _servers[i]._numServed);
				else
					Console.WriteLine("Server {0} Utilization: 0%. Customers served: 0.", i);
			}
			Console.WriteLine("*******************************************************");
		}

    }
}
